《嵌入式 – 深入剖析STM32》STM32高精度延时实现

1前言

在STM32编程过程中经常用到延时函数,最常用的莫过于微秒级延时和毫秒级延时。那么本文针对STM32的延时进行分析和实验。关于STM32的时钟系统,参考笔者博文。

详解STM32时钟系统

2裸机延时

2.1普通延时

这个比较简单,让单片机做一些无关紧要的工作来打发时间,经常用循环来实现,不过要做的比较精准还是要下一番功夫。下面的代码是在网上搜到的,经测试延时比较精准。

//粗延时函数,微秒
void delay_us(u16 time)
{    
   u16 i=0;  
   while(time--)
   {
      i=10;  //自己定义
      while(i--) ;    
   }
}
//毫秒级的延时
void delay_ms(u16 time)
{    
   u16 i=0;  
   while(time--)
   {
      i=12000;  //自己定义
      while(i--) ;    
   }
}

2.2 SysTick 定时器延时

CM3 内核的处理器,内部包含了一个SysTick 定时器,SysTick 是一个24 位的倒计数定时器,当计到0 时,将从RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除,就永不停息。系统定时器一般用于操作系统,用于产生时基,维持操作系统的心跳。SysTick 在STM32的参考手册里面介绍的很简单,其详细介绍,请参阅《Cortex-M3 权威指南》。

1.中断方式
如下,定义延时时间time_delay,SysTick_Config()定义中断时间段,在中断中递减time_delay,从而实现延时。

volatile unsigned long time_delay; // 延时时间,注意定义为全局变量
//延时n_ms
void delay_ms(volatile unsigned long nms)
{
  //SYSTICK分频--1ms的系统时钟中断
  if (SysTick_Config(SystemFrequency/1000))
  {
       while (1);
  }
time_delay=nms;//读取定时时间
  while(time_delay);
  SysTick->CTRL=0x00; //关闭计数器
  SysTick->VAL =0X00; //清空计数器
}
//延时nus
void delay_us(volatile unsigned long nus)
{
  //SYSTICK分频--1us的系统时钟中断
  if (SysTick_Config(SystemFrequency/1000000))
  {
       while (1);
  }
time_delay=nus;//读取定时时间
  while(time_delay);
  SysTick->CTRL=0x00; //关闭计数器
  SysTick->VAL =0X00; //清空计数器
}

//在中断中将time_delay递减。实现延时
void SysTick_Handler(void)
{
if(time_delay)
        time_delay--;
}

还有一种标准写法:

static __IO u32 TimingDelay;

 #define delay_ms(x) delay_us(1000*x)    //单位ms
/**
  * @brief   启动系统滴答定时器 SysTick
  * @param   无
  * @retval  无
  */
void sysTick_init(void)
{
    /* SystemFrequency / 1000    1ms中断一次
     * SystemFrequency / 100000  10us中断一次
     * SystemFrequency / 1000000 1us中断一次
     */
    if (SysTick_Config(SystemCoreClock / 1000000))  // ST3.5.0库版本
    { 
        /* Capture error */ 
        while (1);
    }
        // 关闭滴答定时器  
    SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
}

/** 
  * @brief    us延时程序,1us为一个单位
  * @param    nTime: Delay_us( 1 ) 则实现的延时为 1 * 1us = 1us
  * @retval   无
  */
void delay_us(__IO u32 nTime)
{ 
    TimingDelay = nTime;    

    // 使能滴答定时器  
    SysTick->CTRL |=  SysTick_CTRL_ENABLE_Msk;

    while(TimingDelay != 0);
}

/**
  * @brief      获取节拍程序
  * @param      无
  * @retval     无
  * @attention  在 SysTick 中断函数 SysTick_Handler()调用
  */
void TimingDelay_Decrement(void)
{
    if (TimingDelay != 0x00)
    { 
        TimingDelay--;
    }
}

/**
  * @brief  This function handles SysTick Handler.
  * @param  None
  * @retval None
  */
void SysTick_Handler(void)
{
    TimingDelay_Decrement();    
}

2.非中断方式
SysTick的时钟以 HCLK(AHB 时钟)或 HCLK/8 作为运行时钟,在这里我们选用内部时钟源72M,固定为HCLK 时钟的1/8,所以SYSTICK的时钟为9M,即SYSTICK定时器以9M的频率递减。SysTick 主要包含CTRL、LOAD、VAL、CALIB 等4 个寄存器。

CTRL: SysTick控制和状态寄存器
LOAD: SysTick重装载值寄存器
VAL: SysTick当前值寄存器
CALIB:SysTick校准值寄存器

对这几个寄存器的操作被封装到core_cm3.h中:

s257mF.png

STM32中的Systick 部分内容属于NVIC控制部分,一共有4个寄存器,名称和地址分别是:

 STK_CTRL, 0xE000E010 — 控制寄存器

<center>表1 SysTick控制及状态寄存器</center>

s25bTJ.png

第0位:ENABLE,Systick 使能位 (0:关闭Systick功能;1:开启Systick功能)
第1位:TICKINT,Systick 中断使能位(0:关闭Systick中断;1:开启Systick中断)
第2位:CLKSOURCE,Systick时钟源选择(0:使用HCLK/8 作为Systick时钟;1:使用HCLK作为Systick时钟)
第16位:COUNTFLAG,Systick计数比较标志,如果在上次读取本寄存器后,SysTick 已经数到了0,则该位为1。如果读取该位,该位将自动清零

 STK_LOAD, 0xE000E014 — 重载寄存器

<center>表2 SysTick重装载数值寄存器</center>

s25vSx.png

Systick是一个递减的定时器,当定时器递减至0时,重载寄存器中的值就会被重装载,继续开始递减。STK_LOAD 重载寄存器是个24位的寄存器最大计数0xFFFFFF。

 STK_VAL, 0xE000E018 — 当前值寄存器

<center>表3 SysTick当前数值寄存器</center>

s2IC0e.png

也是个24位的寄存器,读取时返回当前倒计数的值,写它则使之清零,同时还会清除在SysTick 控制及状态寄存器中的COUNTFLAG 标志。

 STK_CALRB, 0xE000E01C — 校准值寄存器

<center>表4 SysTick校准数值寄存器</center>

s2IPTH.png

校准值寄存器提供了这样一个解决方案:它使系统即使在不同的CM3产品上运行,也能产生恒定的SysTick中断频率。最简单的作法就是:直接把TENMS的值写入重装载寄存器,这样一来,只要没突破系统极限,就能做到每10ms来一次 SysTick异常。如果需要其它的SysTick异常周期,则可以根据TENMS的值加以比例计算。只不过,在少数情况下, CM3芯片可能无法准确地提供TENMS的值(如, CM3的校准输入信号被拉低),所以为保险起见,最好在使用TENMS前检查器件的参考手册。

SysTick定时器除了能服务于操作系统之外,还能用于其它目的:如作为一个闹铃,用于测量时间等。要注意的是,当处理器在调试期间被喊停( halt)时,则SysTick定时器亦将暂停运作。

程序如下,相当于查询法。

static u8  fac_us=0;                            //us延时倍乘数              
static u16 fac_ms=0;                            //ms延时倍乘数 

//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
/**
  * @brief  初始化延迟函数
  * @param  None
  * @retval None
  */
void sysTick_init()
{
    //SysTick->CTRL&=0xfffffffb;//bit2清空,选择外部时钟  HCLK/8
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);   //选择外部时钟  HCLK/8
    fac_us=SystemCoreClock/8000000;             //为系统时钟的1/8  
    fac_ms=(u16)fac_us*1000;                    //非OS下,代表每个ms需要的systick时钟数   
}                                   

/**
  * @brief  延时nus
  * @param  nus为要延时的us数.
  * @retval None
  */
void delay_us(u32 nus)
{       
    u32 temp;            
    SysTick->LOAD=nus*fac_us;                    //时间加载           
    SysTick->VAL=0x00;                           //清空计数器
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数    
    do
    {
        temp=SysTick->CTRL;
    }while((temp&0x01)&&!(temp&(1<<16)));     //等待时间到达   
    SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
    SysTick->VAL =0X00;                           //清空计数器     
}

//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对72M条件下,nms<=1864 
/**
  * @brief  延时nms
  * @param  nms为要延时的nms数.
  * @retval None
  */
void delay_ms(u16 nms)
{                 
    u32 temp;          
    SysTick->LOAD=(u32)nms*fac_ms;               //时间加载(SysTick->LOAD为24bit)
    SysTick->VAL =0x00;                          //清空计数器
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数  
    do
    {
        temp=SysTick->CTRL;
    }while((temp&0x01)&&!(temp&(1<<16)));     //等待时间到达   
    SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
    SysTick->VAL =0X00;                          //清空计数器         
}

前文所述的两种方式各有利弊,第一种方式采用库函数,编写简单,由于中断的存在,不利于在其他中断中调用此延时函数,还需要考虑中断嵌套和中断优先级的问题。第二种方式直接操作寄存器,看起来比较繁琐,其实也不难,同时克服了中断方式实现的缺点。

<br>

<br>

<br>

3 RTOS延时

在RTOS中,我们时常需要高精度的定时,一般RTOS都有延时函数,但是不够精确,我们还是用SysTick 定时器,采用寄存器方式进行延时,代码如下:

/* Includes*********************************************************************/
#include "./SysTick/stm32f103_SysTick.h"

#define SYSTEM_SUPPORT_OS       1       //定义系统文件夹是否支持UCOS

//如果使用rt-thread,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "rtthread.h"             //支持OS时,使用 
#endif
//********************************************************************************
static uint32_t fac_us=0;                           //us延时倍乘数

#if SYSTEM_SUPPORT_OS       
    static uint16_t fac_ms=0;                       //ms延时倍乘数,在os下,代表每个节拍的ms数
#endif

#if SYSTEM_SUPPORT_OS                           //如果SYSTEM_SUPPORT_OS定义了,说明要支持OS了(不限于rt-thread).
//当delay_us/delay_ms需要支持OS的时候需要三个与OS相关的宏定义和函数来支持
//首先是3个宏定义:
//delay_osrunning:用于表示OS当前是否正在运行,以决定是否可以使用相关函数
//delay_ostickspersec:用于表示OS设定的时钟节拍,delay_init将根据这个参数来初始哈systick
//delay_osintnesting:用于表示OS中断嵌套级别,因为中断里面不可以调度,delay_ms使用该参数来决定如何运行
//然后是3个函数:
//delay_osschedlock:用于锁定OS任务调度,禁止调度
//delay_osschedunlock:用于解锁OS任务调度,重新开启调度
//delay_ostimedly:用于OS延时,可以引起任务调度.

//本例程仅作RT-Thread的支持,其他OS,请自行参考着移植
//支持RT-Thread
extern volatile rt_uint8_t rt_interrupt_nest;

//在board.c文件的rt_hw_board_init()里面将其置为1
uint8_t OSRunning=0;

#ifdef  RT_THREAD_PRIORITY_MAX                           //RT_THREAD_PRIORITY_MAX定义了,说明要支持RT-Thread 
#define delay_osrunning       OSRunning                //OS是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec RT_TICK_PER_SECOND  //OS时钟节拍,即每秒调度次数
#define delay_osintnesting  rt_interrupt_nest       //中断嵌套级别,即中断嵌套次数
#endif

//us级延时时,关闭任务调度(防止打断us级延迟)
void delay_osschedlock(void)
{
#ifdef RT_THREAD_PRIORITY_MAX
     rt_enter_critical();
#endif  
}

//us级延时时,恢复任务调度
void delay_osschedunlock(void)
{   
#ifdef RT_THREAD_PRIORITY_MAX
      rt_exit_critical();
#endif  
}

//调用OS自带的延时函数延时
//ticks:延时的节拍数
void delay_ostimedly(uint32_t ticks)
{
#ifdef RT_THREAD_PRIORITY_MAX
      rt_thread_delay(ticks);
#endif   
}

#endif

//初始化延迟函数
//当使用ucos的时候,此函数会初始化ucos的时钟节拍
//SYSTICK的时钟固定为AHB时钟
//SYSCLK:系统时钟频率
void delay_init()
{
#if SYSTEM_SUPPORT_OS                       //如果需要支持OS.
    uint32_t reload;
#endif
  SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择外部时钟  HCLK/8
    fac_us=SystemCoreClock/8000000;             //为系统时钟的1/8  
#if SYSTEM_SUPPORT_OS                               //如果需要支持OS.
    reload=SystemCoreClock/8000000;             //每秒钟的计数次数 单位为K        
    reload*=1000000/delay_ostickspersec;    //根据delay_ostickspersec设定溢出时间
                                                              //reload为24位寄存器,最大值:16777216,在180M下,约合0.745s左右    
    fac_ms=1000/delay_ostickspersec;          //代表OS可以延时的最少单位      
    SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;//开启SYSTICK中断
    SysTick->LOAD=reload;                          //每1/OS_TICKS_PER_SEC秒中断一次    
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK
#endif
}                                   

#if SYSTEM_SUPPORT_OS                       //如果需要支持OS.
//延时nus
//nus:要延时的us数.  
//nus:0~190887435(最大值即2^32/fac_us@fac_us=22.5)                                         
void delay_us(uint32_t nus)
{       
    uint32_t ticks;
    uint32_t told,tnow,tcnt=0;
    uint32_t reload=SysTick->LOAD;               //LOAD的值             
    ticks=nus*fac_us;                       //需要的节拍数 
    delay_osschedlock();                    //阻止OS调度,防止打断us延时
    told=SysTick->VAL;                       //刚进入时的计数器值
    while(1)
    {
        tnow=SysTick->VAL;   
        if(tnow!=told)
        {       
            if(tnow<told)tcnt+=told-tnow;    //这里注意一下SYSTICK是一个递减的计数器就可以了.
            else tcnt+=reload-tnow+told;        
            told=tnow;
            if(tcnt>=ticks)break;            //时间超过/等于要延迟的时间,则退出.
        }  
    };
    delay_osschedunlock();                  //恢复OS调度                                                
}  
//延时nms
//nms:要延时的ms数
//nms:0~65535
void delay_ms(uint16_t nms)
{   
    if(delay_osrunning&&delay_osintnesting==0)//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)     
    {        
        if(nms>=fac_ms)                      //延时的时间大于OS的最少时间周期 
        { 
            delay_ostimedly(nms/fac_ms);    //OS延时
        }
        nms%=fac_ms;                        //OS已经无法提供这么小的延时了,采用普通方式延时    
    }
    delay_us((uint32_t)(nms*1000));             //普通方式延时
}

#else  //不用ucos时

//延时nus
//nus为要延时的us数.  
//nus:0~190887435(最大值即2^32/fac_us@fac_us=22.5)   
void delay_us(uint32_t nus)
{       
    uint32_t ticks;
    uint32_t told,tnow,tcnt=0;
    uint32_t reload=SysTick->LOAD;               //LOAD的值             
    ticks=nus*fac_us;                       //需要的节拍数 
    told=SysTick->VAL;                       //刚进入时的计数器值
    while(1)
    {
        tnow=SysTick->VAL;   
        if(tnow!=told)
        {       
            if(tnow<told)tcnt+=told-tnow;    //这里注意一下SYSTICK是一个递减的计数器就可以了.
            else tcnt+=reload-tnow+told;        
            told=tnow;
            if(tcnt>=ticks)break;            //时间超过/等于要延迟的时间,则退出.
        }  
    };
}

//延时nms
//nms:要延时的ms数
void delay_ms(uint16_t nms)
{
    uint32_t i;
    for(i=0;i<nms;i++)
    {   
        delay_us(1000);
    }
}
#endif

以上代码适配RT-Thread实时系统,针对系统嵌入式系统需要进行修改,以上代码包含了裸机的延时函数。值得注意的是,初始化函数在board.c中调用的。

s2IktA.png

【ps】针对RT-Thread官方是有高精度延时方案的,大家也可参考。
RT-Thread时钟系统

Related posts

Leave a Comment